Java IO学习-NIO 粘包与半包
调用 ByteBuffer 的方法
ByteBuffer 调试工具类
这里使用黑马教程的写的工具类 需要先导入 netty 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.66.Final</version>
</dependency>
代码太长了,就放在 Gist 里面了
使用示例:
import static com.alsritter.util.ByteBufferUtil.debugAll;
public class Test {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte) 0x61); // 'a'
debugAll(buffer);
}
}
打印结果如下:
+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 |a......... |
+--------+-------------------------------------------------+----------------+
ByteBuffer 执行方法测试
public class Test {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
String line = "\n===========================================================================================";
System.out.println("向 buffer 中写入1个字节的数据");
buffer.put((byte)97);
// 使用工具类,查看buffer状态
debugAll(buffer);
System.out.println(line);
System.out.println("再向 buffer 中写入4个字节的数据");
buffer.put(new byte[]{98, 99, 100, 101});
debugAll(buffer);
System.out.println(line);
buffer.flip();
System.out.println("取数据前的数据");
debugAll(buffer);
System.out.println(buffer.get());
System.out.println(buffer.get());
System.out.println("查看取得两次数据后 position 的位置");
debugAll(buffer);
System.out.println(line);
System.out.println("使用 compact 切换模式");
buffer.compact();
debugAll(buffer);
System.out.println(line);
System.out.println("再次写入");
buffer.put((byte)102);
buffer.put((byte)103);
debugAll(buffer);
}
}
打印的结果:
向 buffer 中写入1个字节的数据
+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 |a......... |
+--------+-------------------------------------------------+----------------+
===========================================================================================
再向 buffer 中写入4个字节的数据
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
+--------+-------------------------------------------------+----------------+
===========================================================================================
取数据前的数据
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
+--------+-------------------------------------------------+----------------+
97
98
查看取得两次数据后 position 的位置
+--------+-------------------- all ------------------------+----------------+
position: [2], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
+--------+-------------------------------------------------+----------------+
===========================================================================================
使用 compact 切换模式
+--------+-------------------- all ------------------------+----------------+
position: [3], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 63 64 65 64 65 00 00 00 00 00 |cdede..... |
+--------+-------------------------------------------------+----------------+
===========================================================================================
再次写入
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 63 64 65 66 67 00 00 00 00 00 |cdefg..... |
+--------+-------------------------------------------------+----------------+
字符串与 ByteBuffer 的转换
这里主要介绍如何字符串转 ByteBuffer,且设置其编码
bytes to String
编码:字符串调用 getByte 方法获得 byte 数组,将 byte 数组放入 ByteBuffer 中 解码:先调用 ByteBuffer 的 flip 方法,然后通过 StandardCharsets 的 decoder 方法解码
public class Translate {
public static void main(String[] args) {
// 准备两个字符串
String str1 = "hello";
String str2 = "";
ByteBuffer buffer1 = ByteBuffer.allocate(16);
// 通过字符串的 getByte 方法获得字节数组,放入缓冲区中
buffer1.put(str1.getBytes());
// 将缓冲区中的数据转化为字符串
// 切换模式
buffer1.flip();
// 通过 StandardCharsets 解码,获得 CharBuffer,再通过 toString 获得字符串
str2 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str2);
}
}
直接通过编码转
编码:通过 StandardCharsets 的 encode 方法获得 ByteBuffer,此时获得的 ByteBuffer 为读模式,无需通过 flip 切换模式 解码:通过 StandardCharsets 的 decoder 方法解码
public class Translate {
public static void main(String[] args) {
// 准备两个字符串
String str1 = "hello";
String str2 = "";
// 通过 StandardCharsets 的 encode 方法获得 ByteBuffer
// 此时获得的 ByteBuffer 为读模式,无需通过 flip 切换模式
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode(str1);
// 将缓冲区中的数据转化为字符串
// 通过 StandardCharsets 解码,获得 CharBuffer,再通过 toString 获得字符串
str2 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str2);
}
}
通过 wrap 方法
编码:字符串调用 getByte()
方法获得字节数组,将字节数组传给 ByteBuffer 的 wrap()
方法,通过该方法获得ByteBuffer。同样无需调用 flip 方法切换为读模式
解码:通过 StandardCharsets 的 decoder 方法解码
public class Translate {
public static void main(String[] args) {
// 准备两个字符串
String str1 = "hello";
String str2 = "";
// 通过 StandardCharsets 的 encode 方法获得 ByteBuffer
// 此时获得的 ByteBuffer 为读模式,无需通过 flip 切换模式
ByteBuffer buffer1 = ByteBuffer.wrap(str1.getBytes());
// 将缓冲区中的数据转化为字符串
// 通过 StandardCharsets 解码,获得 CharBuffer,再通过 toString 获得字符串
str2 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str2);
}
}
粘包与半包
粘包与半包的现象
网络上有多条数据发送给服务端,数据之间使用 \n
进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有 3 条为
Hello,world\n
I'm Nyima\n
How are you?\n
变成了下面的两个 byteBuffer(粘包,半包)
Hello,world\nI'm Nyima\nHo
w are you?\n
粘包出现原因
发送方在发送数据时,并不是一条一条地发送数据,而是 将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去
半包出现原因
接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将 信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象
解决办法
通过 get(index)
方法遍历 ByteBuffer,遇到分隔符时进行处理。
注意:get(index)
不会改变 position 的值
- 记录该段数据长度,以便于申请对应大小的缓冲区
- 将缓冲区的数据通过
get()
方法写入到 target 中
调用 compact 方法切换模式,因为缓冲区中可能还有未读的数据
public class ByteBufferDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(32);
// 模拟粘包+半包
buffer.put("Hello,world\nI'm Nyima\nHo".getBytes());
// 调用 split 函数处理
split(buffer);
buffer.put("w are you?\n".getBytes());
split(buffer);
}
private static void split(ByteBuffer buffer) {
// 切换为读模式(读模式 position 是从 0 开始的)
buffer.flip();
for(int i = 0; i < buffer.limit(); i++) {
// 遍历寻找分隔符
// get(i) 不会移动 position
if (buffer.get(i) == '\n') {
// 缓冲区长度
int length = i + 1 - buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
// 将前面的内容写入 target 缓冲区
for(int j = 0; j < length; j++) {
// 将 buffer 中的数据写入 target 中
target.put(buffer.get());
}
// 打印查看结果
ByteBufferUtil.debugAll(target);
}
}
// 切换为写模式,但是缓冲区可能未读完,这里需要使用 compact
buffer.compact();
}
}
原理就是,找到分隔符,然后把分隔符前面的内容存储到临时的 target 里面,这个 target 就可以是目标数据,然后把剩下的内容通过 compact 把剩下的内容前移,等待下次取得数据
输出内容为:
+--------+-------------------- all ------------------------+----------------+
position: [12], limit: [12]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 77 6f 72 6c 64 0a |Hello,world. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [10], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 49 27 6d 20 4e 79 69 6d 61 0a |I'm Nyima. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 3f 0a |How are you?. |
+--------+-------------------------------------------------+----------------+